﻿module BoardTool.Sort

open System
open System.Collections.Generic

open BoardTool.Board
open BoardTool.DirTree
open BoardTool.Tool
open BoardTool.Commands

type private GoodBoardInfo = 
    { 
    File : string
    XSize : int
    YSize : int
    Hash : int 
    }

type private BadBoardInfo =
    {
    File : string
    Errors : string[]
    }

type private ParsedBoardInfo =
    | Good of GoodBoardInfo
    | Bad of BadBoardInfo

let private isGood (pbi : ParsedBoardInfo) =
    match pbi with
    | Good _ -> true
    | Bad _ -> false

let private parseBoards (rootDir : string) =
    let parseBoard (file : string) =
        let xySizeOpt = ref None
        let errors = ResizeArray<string>()
        let hash = ref 0

        for bfe in parseBoardFile file |> Array.sort do
            match bfe with
            // Ignore "option exit_roads_to_pavement false"
            | Option( _, _ ) -> ()
            | _ -> 
                hash := !hash * 31 + bfe.GetHashCode()
            match bfe with
            | Size( xSize, ySize ) ->
                xySizeOpt := Some( xSize, ySize )
            | Unrecognized str when str <> "" ->
                errors.Add( str )
            | _ -> ()

        if Option.isNone !xySizeOpt then
            errors.Insert( 0, "Size entry not found" )

        if errors.Count > 0 then 
            ParsedBoardInfo.Bad {
                File = file
                Errors = errors.ToArray()
            }
        else
            let xSize, ySize = Option.get !xySizeOpt
            ParsedBoardInfo.Good { 
                File = file
                XSize = xSize
                YSize = ySize
                Hash = !hash 
            }

    let obs = consoleLogger()
    mapBoards parseBoard obs rootDir

let private TRIMMED_CHARS = [| ' '; '_'; '-'; '('; ')' |]

let private normalizeFileName baseSize file =
    // Decompose
    let dir = IO.Path.GetDirectoryName( file )
    let fileNameNoExt = IO.Path.GetFileNameWithoutExtension( file )
    let ext = IO.Path.GetExtension( file )
            
    // Remove size notation (if present) and various "special" charachers
    let fileNameNoExt = fileNameNoExt.Replace( baseSize, "" )
    let fileNameNoExt = fileNameNoExt.Trim( TRIMMED_CHARS )
    let fileNameNoExt = fileNameNoExt.Replace( "(", "" )
    let fileNameNoExt = fileNameNoExt.Replace( ")", "" )
    let fileNameNoExt = fileNameNoExt.Replace( " -", "-" )
    let fileNameNoExt = fileNameNoExt.Replace( "- ", "-" )
    // Make sure file name is not empty
    let fileNameNoExt = if fileNameNoExt = "" then baseSize + ".board" else fileNameNoExt
            
    // Combine
    let newFileName = fileNameNoExt + ext
    let newFile = IO.Path.Combine( dir, newFileName )

    newFile

type private SortCommand( srcDir, destDir ) =
    
    interface ICommand with
        member this.id = 
            sprintf "(sort %s %s)" srcDir destDir

        member this.run() =
            let mkdir dir =
                if not <| IO.Directory.Exists( dir ) then
                    IO.Directory.CreateDirectory( dir ) |> ignore
        
            let srcRoot = srcDir
            let destRoot = destDir

            mkdir destRoot

            let droppedFiles = ResizeArray<string * string>()
            let renamedFiles = ResizeArray<string * string>()
                        
            let getDestFileName baseSize srcFile file =
                let file = normalizeFileName baseSize file

                // Check for uniqueness
                if not <| IO.File.Exists file then 
                    file
                else
                    let theName = getUniqueFileName file
                    renamedFiles.Add( (srcFile, theName) )
                    theName

            let chooseGood bi =
                match bi with
                | Good bi -> Some bi
                | Bad bi ->
                    let reason = 
                        (sprintf "%d parse errors:\n   " bi.Errors.Length) +
                        (String.concat "\n   " <| Array.sub bi.Errors 0 (min 3 bi.Errors.Length) )

                    droppedFiles.Add( (bi.File, reason) )
                    None

            let dropDuplicates (hash, bis : GoodBoardInfo seq) =
                let isGoodChar ch = 
                    (ch >= 'A' && ch <= 'Z') || 
                    (ch >= 'a' && ch <= 'z') || 
                    (ch >= '0' && ch <= '9') ||
                    (ch = '-' || ch = '.' || ch = '_')

                let getFitness (file : GoodBoardInfo) =
                    let file = file.File
                    let file = IO.Path.GetFileNameWithoutExtension( file )
                    let goodChars = file |> Seq.filter isGoodChar |> Seq.length
                    goodChars - file.Length

                match bis |> Seq.toList with
                | [] ->
                    failwithf "INTERNAL ERROR: Empty files list for a given hash"
                | file :: [] -> 
                    file
                | files ->
                    let bestName = List.maxBy getFitness files
                    for file in files do
                        if file.File <> bestName.File then
                            droppedFiles.Add( (file.File, "Duplicate of " + bestName.File) )
                    bestName

            let copyToSizeDir ((xSize, ySize), bis : GoodBoardInfo seq) =
                let destSubdirName = string(xSize) + "x" + string(ySize)
                let destSubdir = IO.Path.Combine( destDir, destSubdirName )
                
                mkdir destSubdir
                eprintfn "Processing output directory %s" destSubdir

                for bi in bis do
                    let srcFile = bi.File
                    let srcName = IO.Path.GetFileName( srcFile )
                    let destFile = IO.Path.Combine( destSubdir, srcName )
                    let destFile = getDestFileName destSubdirName srcFile destFile
                    IO.File.Copy( srcFile, destFile, true )

            (parseBoards srcRoot).Values
            |> Seq.choose chooseGood
            |> Seq.groupBy (fun bi -> bi.Hash)
            |> Seq.map dropDuplicates
            |> Seq.groupBy (fun bi -> (bi.XSize, bi.YSize))
            |> Seq.iter copyToSizeDir
        
            printfn ""
            printfn "Dropped %d files" droppedFiles.Count
        
            droppedFiles 
            |> Seq.iter (fun (file, reason) -> printfn "%s: %s" file reason)

            printfn ""
            printfn "Renamed %d files" renamedFiles.Count
            renamedFiles
            |> Seq.iter (fun (srcFile, newName) -> printfn "%s: renamed to %s" srcFile newName)


let SORT_COMMAND_FACTORY = 
    { new TransformDirCommandFactory( "sort", "sorts boards by size, removing duplicates" ) with
        override this.createCommand( srcDir, destDir ) =
            SortCommand( srcDir, destDir ) :> ICommand
    } :> ICommandFactory